import * as THREE from 'three';
import { Bone, BufferGeometry, Camera, DirectionalLight, HemisphereLight, Material, Mesh, Object3D, PointLight, Scene, SkinnedMesh, SpotLight,  Texture } from 'three';
import { Command } from './command.js';
import { History as EditorHistory } from './history.js';
import { TezeroMode } from './enums.js';
import { Selector } from './viewport.selector.js';
import { Signal } from './signals.js';
import { TezeroCommponents, TezeroOptions, defaultComponents } from './tezero.js';
import { AddObjectCommand } from './commands/Commands.js';
import {Loader} from './loader';
import {mergeGeometries} from 'three/examples/jsm/utils/BufferGeometryUtils';
import localforage from 'localforage';
import {CSS3DObject,CSS3DSprite} from 'three/examples/jsm/renderers/CSS3DRenderer.js';

const _DEFAULT_CAMERA = new THREE.PerspectiveCamera(50, 1, 0.01, 1000);
_DEFAULT_CAMERA.name = '编辑器';
_DEFAULT_CAMERA.position.set(0, 5, 10);
_DEFAULT_CAMERA.lookAt(new THREE.Vector3());

const _ClIPBOARD_KEY='tezero_clipboard';

interface SignalEvent {
	editScript: Signal<any>;
	startPlayer: Signal<any>;
	stopPlayer: Signal<any>;

	// vr

	toggleVR: Signal<any>;
	exitedVR: Signal<any>;

	// notifications

	editorCleared: Signal<any>;

	savingStarted: Signal<any>;
	savingFinished: Signal<any>;

	transformModeChanged: Signal<any>;
	snapChanged: Signal<any>;
	spaceChanged: Signal<any>;
	rendererCreated: Signal<any>;
	rendererUpdated: Signal<any>;

	sceneBackgroundChanged: Signal<any>;
	sceneEnvironmentChanged: Signal<any>;
	sceneFogChanged: Signal<any>;
	sceneFogSettingsChanged: Signal<any>;
	sceneGraphChanged: Signal<any>;
	sceneRendered: Signal<any>;

	cameraChanged: Signal<any>;
	cameraResetted: Signal<any>;

	geometryChanged: Signal<any>;

	objectSelected: Signal<any>;
	objectFocused: Signal<any>;

	objectAdded: Signal<any>;
	objectChanged: Signal<any>;
	objectRemoved: Signal<any>;

	cameraAdded: Signal<any>;
	cameraRemoved: Signal<any>;

	helperAdded: Signal<any>;
	helperRemoved: Signal<any>;

	materialAdded: Signal<any>;
	materialChanged: Signal<any>;
	materialRemoved: Signal<any>;

	scriptAdded: Signal<any>;
	scriptChanged: Signal<any>;
	scriptRemoved: Signal<any>;

	windowResize: Signal<any>;

	showGridChanged: Signal<any>;
	showHelpersChanged: Signal<any>;
	refreshSidebarObject3D: Signal<any>;
	historyChanged: Signal<any>;

	viewportCameraChanged: Signal<any>;
	viewportShadingChanged: Signal<any>;

	intersectionsDetected: Signal<any>;
}

export enum EditorTheme{
	System='system',
	Light='light',
	Dark='dark'
}


class Editor {

	theme:EditorTheme=EditorTheme.System;

	
	components?:TezeroCommponents=defaultComponents;

	signals: SignalEvent;
	selector: Selector;
	history: EditorHistory;

	loader=new Loader(this);

	project:any={};

	camera: Camera = _DEFAULT_CAMERA.clone();

	scene: Scene = new THREE.Scene();
	sceneHelpers: Scene=new THREE.Scene();
	object = {};
	geometries: { [propname: string]: BufferGeometry } = {};
	materials: { [propname: string]: Material } = {};
	textures: { [propname: string]: Texture } = {};
	scripts: { [propname: string]: string[] } = {};

	materialsRefCounter = new Map(); // tracks how often is a material used by a 3D object

	mixer = new THREE.AnimationMixer(this.scene);

	selected: Object3D | null = null;
	selectedObjects: Object3D[] = [];

	helpers: { [propname: string]: Object3D } = {};

	cameras: { [propname: string]: Camera } = {};

	viewportCamera: Camera = this.camera;
	viewportShading = 'default';

	mode: TezeroMode = TezeroMode.Edit;
	options?: TezeroOptions;
	constructor(options?: TezeroOptions) {
		// const Signal = signals.Signal; 
		this.options = options;
		this.signals = {

			// script

			editScript: new Signal(),

			// player

			startPlayer: new Signal(),
			stopPlayer: new Signal(),

			// vr

			toggleVR: new Signal(),
			exitedVR: new Signal(),

			// notifications

			editorCleared: new Signal(),

			savingStarted: new Signal(),
			savingFinished: new Signal(),

			transformModeChanged: new Signal(),
			snapChanged: new Signal(),
			spaceChanged: new Signal(),
			rendererCreated: new Signal(),
			rendererUpdated: new Signal(),

			sceneBackgroundChanged: new Signal(),
			sceneEnvironmentChanged: new Signal(),
			sceneFogChanged: new Signal(),
			sceneFogSettingsChanged: new Signal(),
			sceneGraphChanged: new Signal(),
			sceneRendered: new Signal(),

			cameraChanged: new Signal(),
			cameraResetted: new Signal(),

			geometryChanged: new Signal(),

			objectSelected: new Signal(),
			objectFocused: new Signal(),

			objectAdded: new Signal(),
			objectChanged: new Signal(),
			objectRemoved: new Signal(),

			cameraAdded: new Signal(),
			cameraRemoved: new Signal(),

			helperAdded: new Signal(),
			helperRemoved: new Signal(),

			materialAdded: new Signal(),
			materialChanged: new Signal(),
			materialRemoved: new Signal(),

			scriptAdded: new Signal(),
			scriptChanged: new Signal(),
			scriptRemoved: new Signal(),

			windowResize: new Signal(),

			showGridChanged: new Signal(),
			showHelpersChanged: new Signal(),
			refreshSidebarObject3D: new Signal(),
			historyChanged: new Signal(),

			viewportCameraChanged: new Signal(),
			viewportShadingChanged: new Signal(),

			intersectionsDetected: new Signal(),

		};


		this.selector = new Selector(this);
		this.scene.name = '场景';
		this.addCamera(this.camera);
		this.history = new EditorHistory(this);
	}


	setScene(scene: Scene) {
		scene.traverse(object=>{
			if(isNaN(object.rotation.x)){
				object.rotation.x=0;
			}
			if(isNaN(object.rotation.y)){
				object.rotation.y=0;
			}
			if(isNaN(object.rotation.z)){
				object.rotation.z=0;
			}
		})
		this.scene.uuid = scene.uuid;
		this.scene.name = scene.name;

		this.scene.background = scene.background;
		this.scene.environment = scene.environment;
		this.scene.fog = scene.fog;
		this.scene.backgroundBlurriness = scene.backgroundBlurriness;
		this.scene.backgroundIntensity = scene.backgroundIntensity;

		this.scene.userData = JSON.parse(JSON.stringify(scene.userData));

		// avoid render per object

		this.signals.sceneGraphChanged.active = false;

		while (scene.children.length > 0) {

			this.addObject(scene.children[0]);

		}

		this.signals.sceneGraphChanged.active = true;

		this.signals.sceneGraphChanged.dispatch(true);

	}

	//

	addObject(object: Object3D, parent?: Object3D, index?: number) {
		// this.outline.addObject(object,parent,index);
		var scope = this;

		object.traverse(function (child: Object3D) {

			if (child instanceof Mesh) {
				if (child.geometry !== undefined) scope.addGeometry(child.geometry);
				if (child.material !== undefined) scope.addMaterial(child.material);
			}

			if (child instanceof Camera) {
				scope.addCamera(child);
			}
			scope.addHelper(child);

		});

		if (parent === undefined) {

			this.scene.add(object);

		} else if (index != undefined) {

			parent.children.splice(index, 0, object);
			object.parent = parent;

		}
		
		this.signals.objectAdded.dispatch(object);
		this.signals.sceneGraphChanged.dispatch();

	}

	moveObject(object: Object3D, parent?: Object3D, before?: Object3D) {
		// this.outline.moveObject(object,parent,before);
		if (parent === undefined) {

			parent = this.scene;

		}

		parent.add(object);

		// sort children array

		if (before !== undefined) {

			var index = parent.children.indexOf(before);
			parent.children.splice(index, 0, object);
			parent.children.pop();

		}

		this.signals.sceneGraphChanged.dispatch();

	}

	nameObject(object: Object3D, name: string) {
		// this.outline.nameObject(object,name);
		object.name = name;
		this.signals.sceneGraphChanged.dispatch();

	}

	removeObject(object: Object3D) {
		
		if (object.parent === null) return; // avoid deleting the camera or scene
		// this.outline.removeObject(object);
		var scope = this;

		object.traverse(function (child) {
			if (child instanceof Camera) {
				scope.removeCamera(child);
			}
			scope.removeHelper(child);

			if (child instanceof Mesh && child.material !== undefined) scope.removeMaterial(child.material);

		});

		object.parent.remove(object);

		this.signals.objectRemoved.dispatch(object);
		this.signals.sceneGraphChanged.dispatch();

	}

	addGeometry(geometry: BufferGeometry) {

		this.geometries[geometry.uuid] = geometry;

	}

	setGeometryName(geometry: BufferGeometry, name: string) {

		geometry.name = name;
		this.signals.sceneGraphChanged.dispatch();

	}

	addMaterial(material: Material) {

		if (Array.isArray(material)) {

			for (var i = 0, l = material.length; i < l; i++) {

				this.addMaterialToRefCounter(material[i]);

			}

		} else {

			this.addMaterialToRefCounter(material);

		}

		this.signals.materialAdded.dispatch();

	}

	addMaterialToRefCounter(material: Material) {

		var materialsRefCounter = this.materialsRefCounter;

		var count = materialsRefCounter.get(material);

		if (count === undefined) {

			materialsRefCounter.set(material, 1);
			this.materials[material.uuid] = material;

		} else {

			count++;
			materialsRefCounter.set(material, count);

		}

	}

	removeMaterial(material: Material) {

		if (Array.isArray(material)) {

			for (var i = 0, l = material.length; i < l; i++) {

				this.removeMaterialFromRefCounter(material[i]);

			}

		} else {

			this.removeMaterialFromRefCounter(material);

		}

		this.signals.materialRemoved.dispatch();

	}

	removeMaterialFromRefCounter(material: Material) {

		var materialsRefCounter = this.materialsRefCounter;

		var count = materialsRefCounter.get(material);
		count--;

		if (count === 0) {

			materialsRefCounter.delete(material);
			delete this.materials[material.uuid];

		} else {

			materialsRefCounter.set(material, count);

		}

	}

	getMaterialById(id: number) {

		var material;
		var materials = Object.values(this.materials);

		for (var i = 0; i < materials.length; i++) {

			if (materials[i].id === id) {

				material = materials[i];
				break;

			}

		}

		return material;

	}

	setMaterialName(material: Material, name: string) {

		material.name = name;
		this.signals.sceneGraphChanged.dispatch();

	}

	addTexture(texture: Texture) {

		this.textures[texture.uuid] = texture;

	}

	//

	addCamera(camera: Camera) {

		if (camera.isCamera) {

			this.cameras[camera.uuid] = camera;

			this.signals.cameraAdded.dispatch(camera);

		}

	}

	removeCamera(camera: Camera) {

		if (this.cameras[camera.uuid] !== undefined) {

			delete this.cameras[camera.uuid];

			this.signals.cameraRemoved.dispatch(camera);

		}

	}

	render(){
		this.signals.rendererUpdated.dispatch(true);
	}

	//

	addHelper(object: Object3D, helper?: Object3D) {
		if (this.mode !== TezeroMode.Edit) return;
		var geometry = new THREE.SphereGeometry(2, 4, 2);
		var material = new THREE.MeshBasicMaterial({ color: 0xff0000, visible: false });

		if (helper === undefined) {

			if (object instanceof Camera) {

				helper = new THREE.CameraHelper(object);

			} else if (object instanceof PointLight) {

				helper = new THREE.PointLightHelper(object, 1);

			} else if (object instanceof DirectionalLight) {

				helper = new THREE.DirectionalLightHelper(object, 1);

			} else if (object instanceof SpotLight) {

				helper = new THREE.SpotLightHelper(object);

			} else if (object instanceof HemisphereLight) {

				helper = new THREE.HemisphereLightHelper(object, 1);

			} else if (object instanceof SkinnedMesh) {

				helper = new THREE.SkeletonHelper(object.skeleton.bones[0]);

			} else if (object instanceof Bone && !(object.parent instanceof Bone)) {
				helper = new THREE.SkeletonHelper(object);

			} else {

				// no helper for this object type
				return;

			}

			const picker = new THREE.Mesh(geometry, material);
			picker.name = 'picker';
			picker.userData.object = object;
			helper.add(picker);

		}

		if (this.sceneHelpers) {
			this.sceneHelpers.add(helper);
			this.helpers[object.id] = helper;

			this.signals.helperAdded.dispatch(helper);
		}


	}

	removeHelper(object: Object3D) {

		if (this.helpers[object.id] !== undefined) {

			var helper = this.helpers[object.id];
			helper.parent?.remove(helper);

			delete this.helpers[object.id];

			this.signals.helperRemoved.dispatch(helper);

		}

	}

	//

	addScript(object: Object3D, script: string) {

		if (this.scripts[object.uuid] === undefined) {

			this.scripts[object.uuid] = [];

		}

		this.scripts[object.uuid].push(script);

		this.signals.scriptAdded.dispatch(script);

	}

	removeScript(object: Object3D, script: string) {

		if (this.scripts[object.uuid] === undefined) return;

		var index = this.scripts[object.uuid].indexOf(script);

		if (index !== - 1) {

			this.scripts[object.uuid].splice(index, 1);

		}

		this.signals.scriptRemoved.dispatch(script);

	}

	getObjectMaterial(object: Mesh, slot: number): Material {

		var material = object.material;

		if (Array.isArray(material) && slot !== undefined) {

			material = material[slot];

		}

		return material as Material;

	}

	setObjectMaterial(object: Mesh, slot: number, newMaterial: Material) {

		if (Array.isArray(object.material) && slot !== undefined) {

			object.material[slot] = newMaterial;

		} else {

			object.material = newMaterial;

		}

	}

	setViewportCamera(uuid: string) {

		this.viewportCamera = this.cameras[uuid];
		this.signals.viewportCameraChanged.dispatch();

	}

	setViewportShading(value: string) {

		this.viewportShading = value;
		this.signals.viewportShadingChanged.dispatch();

	}

	//
	selects(object: Object3D){
		this.selector.selects(object);
	}

	select(object?: Object3D) {

		this.selector.select(object);

	}

	selectById(id: number) {

		if (id === this.camera.id) {

			this.select(this.camera);
			return;

		}
		const object = this.scene.getObjectById(id);

		this.select(object);

	}

	selectByUuid(uuid: string) {

		var scope = this;

		this.scene.traverse(function (child) {

			if (child.uuid === uuid) {

				scope.select(child);

			}

		});

	}

	deselect() {

		this.selector.deselect();

	}

	focus(object?: Object3D) {

		if (object !== undefined) {

			this.signals.objectFocused.dispatch(object);

		}

	}

	focusById(id: number) {

		this.focus(this.scene.getObjectById(id));

	}

	clear() {
		this.history.clear();
		// this.storage.clear();

		this.camera.copy(_DEFAULT_CAMERA);
		this.signals.cameraResetted.dispatch();

		this.scene.name = '场景';
		this.scene.userData = {};
		this.scene.background = null;
		this.scene.environment = null;
		this.scene.fog = null;

		var objects = this.scene.children;

		while (objects.length > 0) {

			this.removeObject(objects[0]);

		}

		this.geometries = {};
		this.materials = {};
		this.textures = {};
		this.scripts = {};

		this.materialsRefCounter.clear();

		// this.animations = {};
		this.mixer.stopAllAction();

		this.deselect();

		this.signals.editorCleared.dispatch();

	}

	//

	async fromJSON(json: any) {
		this.clear();
		var loader = new THREE.ObjectLoader();
		var camera: Camera = await loader.parseAsync(json.camera);

		this.camera.copy(camera);
		this.signals.cameraResetted.dispatch();

		// this.history.fromJSON( json.history );
		this.scripts = json.scripts;
		this.project = json.project;

		// 处理图片没有加载失败
		if(json.scene.images){
			json.scene.images.forEach(element => {
				if(element.url==''){
					element.url={};
				}
			});
		}

		try{
			this.setScene(await loader.parseAsync(json.scene));
			this.scene.traverse((obj)=>{
				// 处理css3object
				if(obj.userData&&obj.userData.html){
					
					const handler=this.components[obj.userData.type];
					if(handler===undefined){
						console.warn(`未知的组件类型: ${obj.userData.type}`);
						return;
					}
					const o=handler(obj.userData.html) as CSS3DObject;
					// const o=cssButton(obj.userData.html) as CSS3DObject;
					const index=obj.parent.children.findIndex((o)=>o.uuid===obj.uuid);
					o.position.copy(obj.position);
					o.scale.copy(obj.scale);
					o.rotation.copy(obj.rotation);
					o.renderOrder = obj.renderOrder;
					const tempId=o.uuid;
					o.uuid=obj.uuid;
					o.name=obj.name;
					obj.uuid=tempId;
					o.userData=obj.userData;
					this.addObject(o,obj.parent,index);
					this.removeObject(obj);

				}
			});
			this.render();
			
		}catch(e){
			console.error('加载场景失败',e);
		}
		

	}

	toJSON() {

		// scripts clean up

		var scene = this.scene;
		var scripts = this.scripts;

		for (var key in scripts) {

			var script = scripts[key];

			if (script.length === 0 || scene.getObjectByProperty('uuid', key) === undefined) {

				delete scripts[key];

			}

		}

		//

		return {
			metadata: {
				version: '0.0.1'
			},
			project: this.project||{},
			camera: this.viewportCamera.toJSON(),
			scene: this.scene.toJSON(),
			scripts: this.scripts,
		};

	}

	objectByUuid(uuid: string) {

		return this.scene.getObjectByProperty('uuid', uuid);

	}

	execute(cmd: Command, optionalName?: string) {

		this.history.execute(cmd, optionalName);

	}

	undo() {

		this.history.undo();

	}

	redo() {

		this.history.redo();

	}

	copy(object?:Object3D){
    return new Promise((resolve,reject)=>{
      const json=(object||this.selected)?.toJSON();
			const text=JSON.stringify(json,(key, value) => {
				if(key==='repeat'&&value===Infinity){
					return -1;
				}
        return value;
      },0)
			// window.localStorage.setItem(_ClIPBOARD_KEY,text);
			localforage.setItem(_ClIPBOARD_KEY,text);
      navigator.clipboard.writeText(text).then(resolve).catch(reject);
    });
  }

  paste(){
    const loader = new THREE.ObjectLoader();
    return new Promise(async (resolve,reject)=>{
      try{
				// let text=window.localStorage.getItem(_ClIPBOARD_KEY);
				var text= await localforage.getItem<string>(_ClIPBOARD_KEY);
				if(text===null){
					text=await navigator.clipboard.readText();
				}
				const jsonObject = JSON.parse(text);
				const object = loader.parse(jsonObject);
				object.uuid=THREE.MathUtils.generateUUID();
				object.name=`${object.name} 复制`;
				if(object instanceof Mesh&&object.userData&&object.userData.isText){
					const mat=object.material.clone();
					mat.uuid=THREE.MathUtils.generateUUID();
					object.material=mat;
				}
        
        this.execute( new AddObjectCommand( this, object ) );
        
        resolve(object);
      }catch(e){
        reject(e);
      }
      
    });
  }

	importFiles(accepts?:string[]){
		const fileInput = document.createElement( 'input' );
		fileInput.multiple = true;
		fileInput.type = 'file';
		fileInput.accept=(accepts||IMPORT_ACCEPTS).join(',');
		fileInput.addEventListener( 'change', ()=> {

			this.loader.loadFiles( fileInput.files );

		} );
		fileInput.click();
	}

	merge(...mesh:Mesh[]):Mesh{
		if(mesh.length<2){
			throw new Error('合并必须多网格');
		}
		let geometryArray = []; // 需要合并的模型数组
  		let materialArray = [];
		let name='';
		// 获取世界位置和旋转
		mesh[0].updateMatrixWorld();

		mesh.forEach(obj=>{
			obj.updateMatrix();
			
			const newGeometry = obj.geometry.applyMatrix4(obj.matrix);
			geometryArray.push(newGeometry);
			materialArray.push(obj.material);
			name+=obj.name;
		})
		let merged = mergeGeometries(geometryArray,true);
		
		mesh[0].updateMatrix();

		let mergeMesh = new THREE.Mesh(merged, materialArray)
		mergeMesh.name=name+' 合并';
		// mergeMesh.position.copy(mesh[0].position);
		mergeMesh.matrixWorld.copy(mesh[0].matrixWorld);
		mergeMesh.updateMatrixWorld();
		return mergeMesh;
	}

};

const IMPORT_ACCEPTS=['.json','.dea','.fbx','.glb','.gltf','.bin','.obj','.mtl','.stl','.3mf']

export { Editor };